feat: AST-based extraction with runtime key parity - #5
Merged
Conversation
Replace the regex string extractor with an @babel/parser AST pass and fix the runtime <T>/<Plural> key building so extracted keys and runtime lookup keys are byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Peyton-Spencer
force-pushed
the
feat/ast-extraction
branch
from
July 19, 2026 20:12
23d5e0b to
75d5c0f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the regex-based string extractor with an AST pass (@babel/parser, TS+JSX plugins, hand-rolled recursive visitor — no @babel/traverse) and fixes the runtime so extraction keys and runtime lookup keys are byte-identical.
The parity contract
A
<T>key is built the same way on both sides:trimWhitespace(the transform babel-preset-solid applies at compile time):\rremoved; if the text contains a newline — continuation lines lose leading whitespace, whitespace-only lines are dropped, lines join with a single space; finally all whitespace runs collapse to one space. HTML entities are decoded after collapsing (extractor ships numeric + common named entity decoding).{"text"},{5},{`text`},{"a" + "b"},{-5}) are inlined by the compiler and merge into the surrounding text — extraction folds them into the key the same way.<Var>,<Num>,<Currency>,<DateTime>, arbitrary elements/components) and dynamic expressions the compiler wraps in a memo (calls, member access, tagged templates, …) become ordered{0},{1}, … placeholders, in document order.{null}/{undefined}/ boolean literals render nothing and contribute nothing.id/contextare read via AST — any quote style, expression-container strings, attributes containing>all work.Expressions whose runtime type cannot be known statically (bare identifiers, identifier-only conditionals, interpolated template literals) are inlined raw by the compiler — a string value would merge into the runtime key while an element would become a slot — so the whole
<T>is skipped with afile:linewarning telling the author to wrap the value in<Var>.Defect classes fixed (regex extractor vs runtime)
msg()and<T>text were silently skippedmsg(`template literal`)skipped<T>Hello {name()}</T>extracted the literal{name()}text instead ofHello {0}<T>Click <a …>here</a></T>extracted raw HTML instead ofClick {0}<T>kept raw newlines/indentation while the compiler collapses whitespace>(e.g.params={{ ok: a > b }}) corrupted the match<T id="k"/>skipped<Plural>forms never extractedRuntime changes (required for parity)
<T>no longer usesresolveChildren(). Solid'schildren()helper resolves the compiler's memo wrappers and<Var>thunks to their values, so a dynamic string child collapsed into literal text and the{0}key was never built — placeholder translations were dead on arrival.<T>now reads raw children (flattening arrays only): strings/numbers are text,null/undefined/booleans are skipped, everything else (functions, elements) is an ordered slot. Reactivity improves: slot functions are passed through to Solid'sinsertfor fine-grained updates.<Plural>now consults the dictionary: a selected string form is translated with the source string as the key and supports an{n}placeholder interpolated with the count (other="{n} items"). Non-string forms render as-is.Warnings
extractStringsFromSource(code, filePath, warnings?)takes an optional collector (return shape unchanged); the CLI and Vite plugin print eachfile:line — message. Covered: dynamicmsg()args, spread props on<T>, dynamicid/context, statically-unknowable<T>children, non-literal<Plural>forms, unparseable files.Verification
tests/runtime-parity.test.ts: every fixture is compiled with the real Solid compiler (babel-preset-solid, dom-expressions DOM output), rendered into happy-dom with a dictionary keyed by whateverextractStringsFromSourceproduced for the same source, asserting the translated text appears — covering defects a–g, entity decoding, static-literal merging, slot reordering, edge whitespace,<Plural>selection/interpolation, no-provider passthrough, and locale switching.bun run buildandbunx tsc --noEmitclean.Notes / deviations
@babel/parseradded as a real dependency and bundled intodist/cli.js/dist/vite.jsvia tsupnoExternal(runtimedist/index.jsuntouched — 0 bytes of Babel).babel-preset-solid+@babel/coreadded as devDependencies (integration tests compile fixtures with the real compiler).bun test --conditions=browser(solid-js otherwise resolves to its server build under Bun and DOM rendering is impossible); CI workflows switched frombun testtobun run testaccordingly.src/cli.tshad its own#!line on top of the tsup banner, making the publisheddist/cli.jsa syntax error when executed with node directly.🤖 Generated with Claude Code